<?php

require_once dirname(__FILE__) . '/SmEngines/ISmEngine.itf.php';
defined('DS') || define('DS', DIRECTORY_SEPARATOR);

class SmTemplate
{
    # Mảng lưu trữ các giá trị được gán vào template
    protected $_storage = array();

    # Lưu trữ các đường dẫn đến tập tin CSS & Js
    protected $_cssFiles = array();
    protected $_jsFiles = array();

    # Chứa các cặp tên + thẻ mở các tag
    protected $engines = array();

    protected $_buffer = '';

    public static $Configs = array(
        'cache' => false,
        'cache_dir' => 'template_cache',
        'cache_timeOut' => 7200,
        'log' => 'template_Log.txt',
        'debug' => true
    );

    /**
     * Đặt cấu hình chung cho template
     * @static
     * @param $name array|string
     * @param null $value
     */
    public static function config($name, $value = null)
    {
        if (is_array($name)) {
            self::$Configs = array_merge(self::$Configs, $name);
        } else {
            self::$Configs[$name] = $value;
        }
    }

    public function __construct()
    {
        $this->loadEngines();
    }

    private function loadEngines()
    {

        $engineFilePattern = dirname(__FILE__) . DS . 'SmEngines' . DS . '*Tag.class.php';
        foreach (glob($engineFilePattern) as $file) {
            $class = basename($file, '.class.php');
            include_once $file;
            if (class_exists($class, false)) {
                $o = new $class('#');
                $this->engines[] = array(
                    'name' => $class,
                    'tag' => $o->getTag()
                );
                unset($o);
            } else {
                self::log("Load engine [{$class}][{$file}] failed!");
            }
        }
    }

    public static function log($content)
    {
        $str = "[" . date('d/m/Y h:m') . "] " . $content . "\n";
        file_put_contents(self::$Configs['Log'], $str, FILE_APPEND);
    }

    public static function debug($data)
    {
        is_array($data) || $data = array($data);
        self::$Configs['debug'] && self::log(print_r($data, true));
    }

    public function preCompile($file)
    {
        if (!is_writable(self::$Configs['cache_dir'])) {
            die("Cache directory [" . self::$Configs['cache_dir']. "] is not ready for writing!");
        }


        if (!is_readable($file)) {
            $file_zend_style = VIEWS_DIR . str_replace('_', '/', $file) . '.tpl';
            if (!is_readable($file_zend_style)) {
                throw new Exception("Template file [{$file}] is not ready for reading!", 10102);
            } else {
                $file = $file_zend_style;
            }
        }


        $this->_buffer = file_get_contents($file);
        $content =& $this->_buffer;
        # process include statement
        $pattern = "/{include (.*?)}/";
        while (preg_match($pattern, $content, $match)) {
            $file = VIEWS_DIR . $match[1] . '.tpl';
            $file_zend_style = VIEWS_DIR . str_replace('_', '/', $match[1]) . '.tpl';
            if (file_exists($file)) {
                $content = str_replace($match[0], file_get_contents($file), $content);
            } elseif (file_exists($file_zend_style)) {
                $content = str_replace($match[0], file_get_contents($file_zend_style), $content);
            } else {
                throw new Exception("Including file [{$file}] is not exist!");
            }
        }

        # process css statement
        $pattern = "/{css (.*?)( print)?}/";
        while (preg_match($pattern, $content, $match)) {

            $this->css($match[1],
                isset($match[2]) ? 'print' : 'screen');
            $content = str_replace($match[0], null, $content);
        }

        $pattern = "/{js (.*?)}/";
        while (preg_match($pattern, $content, $match)) {
            $this->js($match[1]);
            $content = str_replace($match[0], null, $content);
        }
    }

    public function compile($file = null)
    {

        if ($file) {
            $this->preCompile($file);
        }

        $data = array(
            new OutTag('#')
        );

        $depth = 0;
        $currPos = 0;

        $content =& $this->_buffer;

        # process comments
        $content = preg_replace('/{#.*?}/', null, $content);

        $content_len = strlen($content);
        while ($currPos < $content_len) {
            # browsing engines
            foreach ($this->engines as $e) {
                $openTagPattern = '/^' . '(?<!\\\\){' . $e['tag'] . '\s?(?P<tag>.*?)' . '(?<!\\\\)}' . '/';
                #self::debug('Tag-Pattern: ' . $openTagPattern);
                if (preg_match($openTagPattern, substr($content, $currPos), $match)) {
                    $depth++;
                    $data[$depth] = new $e['name']($match['tag']);

                    $currPos += strlen($match[0]);

                    # Check current engine has close tag?
                    if ($data[$depth]->hasClose() === FALSE) {
                        $data[$depth - 1]->append($data[$depth]->render());
                        unset($data[$depth]);
                        $depth--;
                    }

                    # continue while
                    continue 2;
                }
            }

            if ($depth > 0) {
                # Check close tag
                $closeTagPattern = '/^' . '(?<!\\\\){\/' . $data[$depth]->getTag() . '(?<!\\\\)}/';
                if (preg_match($closeTagPattern, substr($content, $currPos), $match)) {
                    $currPos += strlen($match[0]);
                    $data[$depth - 1]->append($data[$depth]->render());
                    unset($data[$depth]);
                    $depth--;

                    continue;
                }
            }

            $data[$depth]->append($content[$currPos++]);
        } # /while

        $html = $data[$depth]->render();

        $html = str_replace('</head>', $this->renderCssJs() . '</head>', $html);

        $root = str_replace('/', '\/', ROOT_URL);
        $pattern = '/((?:href|src|action)=)"(?!http|' . $root .'|mailto)\/?(.*?|)"/m';

        return preg_replace($pattern, "$1" . '"' . ROOT_URL . "$2" . '"', $html);
    }

    public function render($file, $useCache = false, $cacheThis = true)
    {
        $nFilename = str_replace(
            array(':', '\\', '/', '.'),
            array('$', '$', '$', '-'),
            $file
        );

        $compiledFile = self::$Configs['cache_dir'] . '/' . $nFilename . '.cache.php';

        ob_start();
        extract($this->_storage);
        if ($useCache && file_exists($compiledFile) &&
            (filemtime($compiledFile) + self::$Configs['cache_timeout']) < time()) {
            include $compiledFile;
        } else {

            $content = $this->compile($file);
            if (!$cacheThis) {
                $compiledFile .= uniqid('.tmp-');
            }
            file_put_contents($compiledFile, $content);
            include $compiledFile;
            if (!$cacheThis) {
                unlink($compiledFile);
            }
        }

        $content = ob_get_contents();
        ob_end_clean();

        return $content;
    }

    public function display($file = null)
    {
        if ($file) {
            $this->preCompile($file);
        }
        echo $this->render($file, self::$Configs['cache']);
    }

    public function assign($name, $value = null)
    {
        if (is_array($name)) {
            $this->_storage = array_merge($this->_storage, $name);
        } else {
            $this->_storage[$name] = $value;
        }
    }

    public function css($file, $media)
    {
        foreach ($this->_cssFiles as &$css) {
            if ($css['file'] == $file) {
                $css['media'] = $media;
                return;
            }
        }


        $this->_cssFiles[] = array(
            'file' => $file,
            'media' => $media
        );
    }

    public function js($file)
    {
        $this->_jsFiles[] = $file;
    }

    private function renderCssJs()
    {
        $output = '';
        foreach ($this->_cssFiles as $css) {
            $output .= "<link rel=\"stylesheet\" type=\"text/css\" href=\"{$css['file']}\" media=\"{$css['media']}\"/>\n";
        }

        foreach ($this->_jsFiles as $js) {
            $output .= "<script type=\"text/javascript\" src=\"{$js}\"></script>\n";
        }

        return $output;
    }

    public function json()
    {
        return json_encode($this->_storage);
    }

    public function getCss()
    {
        return $this->_cssFiles;
    }

    public function getJs()
    {
        return $this->_jsFiles;
    }
}